﻿using System;
using System.IO;
using ooWii.Collections;
using ooWii.Security;

namespace ooWii.IO
{
	public class WiiPartition
	{
		#region WiiPartition

		Stream iso;
		long partitionOffset;
		long dataOffset;
		long decryptedSize;
		UInt32 type;
		CertificateChain certificateChain;
		Ticket ticket;
		Tmd tmd;
		byte[] h3Table;
		SHA1Engine sha1Engine;
		AESEngine aesEngine;

		const int ticketSize = 0x2a4;
		const int h3TableSize = 0x18000;
		const int clusterSize = 0x8000;
		const int dataBytesPerCluster = 0x7c00;

		public static WiiPartition Create( Stream iso, long partitionInfoTableOffset, SHA1Engine sha1Engine, AESEngine aesEngine )
		{
			WiiPartition wiiPartition = new WiiPartition( sha1Engine, aesEngine );

			wiiPartition.iso = iso;

			byte[] partitionInfoTableBuffer = new byte[ 8 ];

			iso.Position = partitionInfoTableOffset;
			if( iso.Read( partitionInfoTableBuffer, 0, partitionInfoTableBuffer.Length ) != partitionInfoTableBuffer.Length )
			{
				throw new WiiException( "Cannot read partition info table" );
			}

			long partitionOffset = WiiBuffer.GetUInt34( partitionInfoTableBuffer, 0 );
			wiiPartition.partitionOffset = partitionOffset;
			wiiPartition.type = WiiBuffer.GetUInt32( partitionInfoTableBuffer, 4 );

			byte[] offsetBuffer = new byte[ 28 ];

			iso.Position = partitionOffset + ticketSize;
			if( iso.Read( offsetBuffer, 0, offsetBuffer.Length ) != offsetBuffer.Length )
			{
				throw new WiiException( "Cannot read partition offsets" );
			}

			UInt32 tmdSize = WiiBuffer.GetUInt32( offsetBuffer, 0 );
			long tmdOffset = WiiBuffer.GetUInt34( offsetBuffer, 4 );
			UInt32 certChainSize = WiiBuffer.GetUInt32( offsetBuffer, 8 );
			long certChainOffset = WiiBuffer.GetUInt34( offsetBuffer, 12 );
			long h3TableOffset = WiiBuffer.GetUInt34( offsetBuffer, 16 );
			wiiPartition.dataOffset = WiiBuffer.GetUInt34( offsetBuffer, 20 );
			long dataSize = WiiBuffer.GetUInt34( offsetBuffer, 24 );

			wiiPartition.decryptedSize = ( dataSize / clusterSize ) * dataBytesPerCluster;

			SparseStream ticketStream = new SparseStream( iso );
			ticketStream.Map( 0, partitionOffset, 0x2a4 );
			ticketStream.SetLength( 0x2a4 );

			wiiPartition.ticket = Ticket.Create( ticketStream, aesEngine );

			SparseStream tmdStream = new SparseStream( iso );
			tmdStream.Map( 0, partitionOffset + tmdOffset, tmdSize );
			tmdStream.SetLength( tmdSize );

			wiiPartition.tmd = Tmd.Create( tmdStream, sha1Engine );

			SparseStream certificateChainStream = new SparseStream( iso );
			certificateChainStream.Map( 0, partitionOffset + certChainOffset, certChainSize );
			certificateChainStream.SetLength( certChainSize );

			wiiPartition.certificateChain = CertificateChain.Create( certificateChainStream, sha1Engine );

			byte[] h3Table = new byte[ h3TableSize ];

			iso.Position = partitionOffset + h3TableOffset;
			if( iso.Read( h3Table, 0, h3Table.Length ) != h3Table.Length )
			{
				throw new WiiException( "Cannot read h3 table" );
			}

			wiiPartition.h3Table = h3Table;

			return wiiPartition;
		}

		WiiPartition( SHA1Engine sha1Engine, AESEngine aesEngine )
		{
			this.sha1Engine = sha1Engine;
			this.aesEngine = aesEngine;
		}

		public UInt32 Type
		{
			get
			{
				return type;
			}
		}

		public CertificateChain CertificateChain
		{
			get
			{
				return certificateChain;
			}
		}

		public Ticket Ticket
		{
			get
			{
				return ticket;
			}
		}

		public Tmd Tmd
		{
			get
			{
				return tmd;
			}
		}

		public void VerifyHash()
		{
			if( tmd.Content.Count != 1 )
			{
				throw new WiiException( "Invalid TMD content" );
			}

			if( !tmd.Content[ 0 ].VerifyHash( h3Table ) )
			{
				throw new WiiException( "h3 hash doesn't match TMD" );
			}
		}

		public WiiPartitionStream OpenRead()
		{
			return new WiiPartitionStream( iso, partitionOffset + dataOffset, decryptedSize, ticket, h3Table, sha1Engine, aesEngine );
		}

		#endregion
	}
}
